﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SSharp.Core.Evaluator;
using SSharp.Core.DataTypes;
using System.Reflection;
using SSharp.Core.Util;
using System.Runtime.CompilerServices;
using Microsoft.CSharp.RuntimeBinder;

namespace SSharp.Core.Builtins.Macros {
	internal interface DotNetObjectWrapper {
		/// <summary>
		/// Gets the wrapped object
		/// </summary>
		Object UnderlyingObject { get; }
	}

	public class DotNetObject<T> : Macro, DotNetObjectWrapper {
		private static readonly Cache<Symbol, MemberInfo[]> members;
		private static readonly Cache<Symbol, Func<object, object>> getters;
		private static readonly Cache<Symbol, Func<object, object>> setters;

		static DotNetObject() {
			members = new Cache<Symbol, MemberInfo[]>(GetMembers);
			getters = new Cache<Symbol, Func<object, object>>(name => MemberAccess(name, false));
			setters = new Cache<Symbol, Func<object, object>>(name => MemberAccess(name, true));
		}

		/// <summary>
		/// The wrapped object
		/// </summary>
		private readonly T obj;

		public DotNetObject(T obj) {
			this.obj = obj;
		}

		public object Expand(IList<object> args) {
			return Expand(args, false);
		}

		/// <summary>
		/// Expands the macro, allowing for setter expansion.
		/// When called from the Set macro, isSetter should be true and a setter will be returned instead of a value.
		/// </summary>
		public object Expand(IList<object> args, bool isSetter) {
			if (args.Count != 1) {
				throw new SyntaxError(".Net object expects one argument, given " + args.Count);
			}

			Symbol name = args[0] as Symbol;
			if (name == null) {
				throw new SyntaxError(".Net object expects one argument, given " + args.Count);
			}

			if (isSetter) {
				return setters[name](obj);
			} else {
				// note that the getter is a method that is applied here
				return getters[name](obj);
			}
		}

		/// <summary>
		/// Returns a getter procedure for 
		/// </summary>
		/// <param name="name"></param>
		/// <param name="isSetter"></param>
		/// <returns></returns>
		public static Func<object, object> MemberAccess(Symbol name, bool isSetter) {
			MemberInfo[] m = members[name];
			if (m.Length == 0) {
				throw new UnboundedVariableException("unkown member " + typeof(T).Name + "." + name);
			}

			if (m[0] is FieldInfo) {
				if (m.Length > 1) {
					throw new InvalidOperationException("More than one member with given name exists. " + typeof(T).Name + "." + name);
				}
				return ExpandField((FieldInfo)m[0], isSetter);
			}

			if (m[0] is PropertyInfo) {
				if (m.Length > 1) {
					throw new InvalidOperationException("More than one member with given name exists. " + typeof(T).Name + "." + name);
				}
				return ExpandProperty((PropertyInfo)m[0], isSetter);
			}

			if (m[0] is MethodInfo) {
				if (m.Any(member => !(member is MethodInfo))) {
					// one of the members is not a method
					throw new InvalidOperationException("More than one member with given name exists. " + typeof(T).Name + "." + name);
				}

				if (isSetter) {
					throw new InvalidOperationException("Cannot set the value of a method field!" + typeof(T).Name + "." + name);
				}

				return ExpandMethod(name.ToString());
			}

			throw new InvalidOperationException("Unknown member type. " + typeof(T).Name + "." + name);
		}

		/// <summary>
		/// Returns the Expand result for a FieldInfo
		/// </summary>
		private static Func<object, object> ExpandField(FieldInfo info, bool isSetter) {
			if (isSetter) {
				string methodName = "set_" + info.Name;
				return obj =>
					new PrimitiveProcedure(methodName,
						args => {
							if (args.Length != 1) {
								throw new ApplicationError(methodName + " expected one argument, given " + args.Length);
							}
							info.SetValue(obj, args[0]);
							return null;
						});
			} else {
				return obj => DotNetObject.WrapIfNeeded(info.GetValue(obj));
			}
		}

		/// <summary>
		/// Returns the Expand result for a PropertyInfo
		/// </summary>
		private static Func<object, object> ExpandProperty(PropertyInfo info, bool isSetter) {
			if (info.GetIndexParameters().Length == 0) {
				// normal property
				if (isSetter) {
					string methodName = "set_" + info.Name;
					return obj =>
						new PrimitiveProcedure(methodName,
							args => {
								if (args.Length != 1) {
									throw new ApplicationError(methodName + " expected one argument, given " + args.Length);
								}
								info.SetValue(obj, args[0], new object[0]);
								return null;
							});
				} else {
					return obj => DotNetObject.WrapIfNeeded(info.GetValue(obj, new object[0]));
				}
			} else {
				throw new NotImplementedException("Indexed properties are not implemented");
			}
		}

		/// <summary>
		/// Returns the Expand result for when the MemberInfo[] were all MethodInfos
		/// </summary>
		private static Func<object, object> ExpandMethod(string name) {
			return obj =>
				new PrimitiveProcedure(name,
					args =>
						DotNetObject.WrapIfNeeded(typeof(T).InvokeMember(name,
							BindingFlags.Instance
							| BindingFlags.Public
							| BindingFlags.InvokeMethod
							| BindingFlags.OptionalParamBinding,
							null,
							obj,
							args.Select(DotNetObject.Unwrap).ToArray())));
		}

		private static MemberInfo[] GetMembers(Symbol name) {
			return typeof(T).GetMember(name.ToString(),
				BindingFlags.Instance
				| BindingFlags.Public);
		}

		public override string ToString() {
			return obj.ToString();
		}

		public object UnderlyingObject {
			get { return obj; }
		}
	}

	/// <summary>
	/// Provides methods to wrap an object with a DotNetObject macro
	/// </summary>
	public static class DotNetObject {
		private static readonly Cache<Type, ConstructorInfo> dotNetObjectMacroConstructor;

		static DotNetObject() {
			dotNetObjectMacroConstructor = new Cache<Type, ConstructorInfo>(GetDotNetObjectMacroConstructor);
		}

		/// <summary>
		/// Returns the constructor for a DotNetObject typed over the given type
		/// </summary>
		private static ConstructorInfo GetDotNetObjectMacroConstructor(Type type) {
			return typeof(DotNetObject<>)
				.MakeGenericType(type)
				.GetConstructor(new[] { type });
		}

		/// <summary>
		/// If obj can be accessed by scheme directly, returns obj.
		/// If obj refers to a dotnet object that needs to be wrapped, returns new DotNetObject[typeof(obj)](obj)
		/// </summary>
		public static object WrapIfNeeded(object obj) {
			if (obj == null
				|| obj is int
				|| obj is string
				|| obj is Cons
				|| obj is Macro
				|| obj is Procedure
				|| obj is DotNetObjectWrapper) {
				// obj can be accessed directly by scheme; no need to wrap it
				return obj;
			} else {
				return Wrap(obj);
			}
		}

		/// <summary>
		/// Wraps the given object in a DotNetObject[typeof(obj)]
		/// </summary>
		public static object Wrap(object obj) {
			Type type = obj.GetType();
			ConstructorInfo constructor = dotNetObjectMacroConstructor[type];
			return constructor.Invoke(new[] { obj });
		}

		/// <summary>
		/// Unwraps an DotNetObject macro around the object, if there is one.
		/// </summary>
		public static object Unwrap(object obj) {
			DotNetObjectWrapper wrapper = obj as DotNetObjectWrapper;
			if (wrapper != null) {
				return wrapper.UnderlyingObject;
			} else {
				// obj isn't wrapped
				return obj;
			}
		}
	}
}
